# This file is part of Gehyra.
#
# Gehyra is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Gehyra is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gehyra.  If not, see <http://www.gnu.org/licenses/>.

"""
@package gehyra.config.iniconfig
A parser for INI files that preserves comments between saves.
$Id: iniconfig.py 450 2011-01-20 14:28:47Z andyhhp@gmail.com $
@todo: comment this file
"""

"""
@file gehyra/config/iniconfig.py
A parser for INI files that preserves comments between saves.
"""

## @cond

from gehyra.common.linkedlist import llist, NIL

from ConfigParser import (DEFAULTSECT, RawConfigParser,
			  MissingSectionHeaderError, ParsingError)
from collections import MutableMapping
from ast import literal_eval
import re

SECTCRE = RawConfigParser.SECTCRE
#copypasta from RawConfigParser.OPTCRE to include DOTALL
OPTCRE = re.compile(
    r'(?P<option>[^:=\s][^:=]*)'  # very permissive!
    r'\s*(?P<vi>[:=])\s*'         # any number of space/tab,
                                  # followed by separator
                                  # (either : or =), followed
                                  # by any # space/tab
    r'(?P<value>.*)$',            # everything up to eol
        re.DOTALL
    )


def force_type(string):
    """force type"""
    try:
        return literal_eval(string.lstrip())
    except (ValueError, SyntaxError):
        return string


class INIConfig(object):
    """
    Provides a view of an INI file, using INISection to preserve comments
    and whitespace.
    """

    def __init__(self):
        self._lines = None
        self._defaults = None
        self._sections = None

    def load(self, fin):
        """Load config"""
        self._lines = llist.from_list(fin.readlines())
        self._sections = {}
        self._defaults = INISection(DEFAULTSECT, NIL, self)

        try:
            filename = fin.name
        except AttributeError:
            filename = '<???>'

        self.__parse_lines(fpname=filename)

    def save(self, fout):
        """Save config"""
        fout.writelines(self._lines)

    def itersections(self):
        """itersections"""
        return self._sections.iteritems()

    def update(self, cfg):
        """update"""
        ## TODO update _defaults too
        for s, sect in cfg.itersections():
            own = self._sections[s]
            for k in sect:
                own[k] = sect[k]

    def render_value(self, key, value):
        """render_value"""
        return "%s = %s\n" % (key, value)

    def __parse_lines(self, fpname='<???>'):
        """parse_lines"""
        optname = None
        cursect = None
        e = None

        for lineno, node in enumerate(self._lines.iternodes(), 1):
            line = node.val

            # following code lifted from ConfigParser._read with modifications

            # comment or blank line?
            if line.strip() == '' or line[0] in '#;':
                continue
            if line.split(None, 1)[0].lower() == 'rem' and line[0] in "rR":
                # no leading whitespace
                continue
            # continuation line?
            if line[0].isspace() and cursect is not None and optname:
                cursect.push_item(optname, node)
            # a section header or option header?
            else:
                # is it a section header?
                mo = SECTCRE.match(line)
                if mo:
                    sectname = mo.group('header')
                    if sectname in self._sections:
                        cursect = self._sections[sectname]
                    elif sectname == DEFAULTSECT:
                        cursect = self._defaults
                    else:
                        cursect = INISection(sectname, node, self)
                        self._sections[sectname] = cursect
                    # So sections can't start with a continuation line
                    optname = None
                # no section header in the file?
                elif cursect is None:
                    raise MissingSectionHeaderError(fpname, lineno, line)
                # an option line?
                else:
                    mo = OPTCRE.match(line)
                    if mo:
                        #optname, vi, optval = mo.group('option', 'vi', 'value')
                        optname, _, _ = mo.group('option', 'vi', 'value')
                        optname = self.optionxform(optname.rstrip())
                        cursect.push_item(optname, node)
                    else:
                        # a non-fatal parsing error occurred.  set up the
                        # exception but keep going. the exception will be
                        # raised at the end of the file and will contain a
                        # list of all bogus lines
                        if not e:
                            e = ParsingError(fpname)
                        e.append(lineno, repr(line))
        # if any parsing errors occurred, raise an exception
        # pylint: disable=E0702
        # lint cant figure out that this will never actually raise None
        if e:
            raise e
        # pylint: enable=E0702

    def optionxform(self, optionstr):
        """optionxform"""
        return optionstr.lower()


class INISection(MutableMapping, dict):
    """
    Provides a dict-type view of a section in an INI file. Value parsing is
    done on the fly, which allows comments and whitespace to be preserved.
    """

    def __init__(self, sectname, head, cfg):
        super(INISection, self).__init__()
        self.__dict__["__cfg"] = cfg
        # a dict of (start, end) (inclusive) lines for each key-value pair
        self.__dict__["__ranges"] = {}

    def push_item(self, key, node):
        """Extend the range of lines for the given key."""
        ranges = self.__dict__["__ranges"]
        if key not in ranges:
            ranges[key] = (node, node)
        else:
            ranges[key] = (ranges[key][0], node)

    def __contains__(self, key):
        ranges = self.__dict__["__ranges"]
        return key in ranges

    def __len__(self):
        ranges = self.__dict__["__ranges"]
        return len(ranges)

    def __iter__(self):
        ranges = self.__dict__["__ranges"]
        return iter(ranges)

    def __getitem__(self, key):
        a, b = self.__dict__["__ranges"][key]
        line = a.val

        # process first line
        # code lifted from ConfigParser._read
        mo = OPTCRE.match(line)
        assert not not mo
        #optname, vi, optval = mo.group('option', 'vi', 'value')
        _, vi, optval = mo.group('option', 'vi', 'value')
        if vi in ('=', ':') and ';' in optval:
            # ';' is a comment delimiter only if it follows
            # a spacing character
            pos = optval.find(';')
            if pos != -1 and optval[pos-1].isspace():
                optval = optval[:pos]
        optval = optval.strip()

        # process continuations
        # code lifted from ConfigParser._read
        for node in a.next.iternodes(b.next):
            line = node.val
            value = line.strip()
            if value:
                optval = "%s\n%s" % (optval, value)

        #return force_type(optval)
        return optval

    def __setitem__(self, key, value):
        a, b = self.__dict__["__ranges"][key]
        a.val = self.__dict__["__cfg"].render_value(key, value)
        a.next = b.next
        return

    def __delitem__(self, key):
        ranges = self.__dict__["__ranges"]
        a, b = ranges[key]
        a.val = ""
        a.next = b.next
        del ranges[key]

    def __str__(self):
        return str(dict(self.iteritems()))

    #__repr__ = MutableMapping.__repr__
        



# if __name__ == "__main__":
#     import sys
#     III = INIConfig()
#     III.load(open("defaults/node.cfg"))
#     print "%r" % III._sections

#     print "%s" % III._sections["network"]
#     III.save(sys.stdout)

#     III._sections["network"]["network"] = "loldongs"
#     print "%s" % III._sections["network"]
#     III.save(sys.stdout)

#     del III._sections["network"]["network"]
#     print "%s" % III._sections["network"]
#     III.save(sys.stdout)

## @endcond
